/*
 * @(#) $Header$
 *
 * Copyright (C) 2008  Forklabs Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.media.jai.opimage;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import javax.media.jai.OpImage;
import javax.media.jai.ROI;
import javax.media.jai.StatisticsOpImage;
import com.sun.media.jai.opimage.ExtremaOpImage;
import ca.forklabs.media.jai.operator.PrecisionAndRecallDescriptor;

/**
 * Class {@code PrecisionAndRecallOpImage} is an {@link OpImage} implementing
 * the <em>PrecisionAndRecall</em> operation.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.media.jai.opimage.PrecisionAndRecallOpImage">Daniel Léonard</a>
 * @version $Revision$
 * @see PrecisionAndRecallDescriptor
 * @see PrecisionAndRecallRIF
 */
@SuppressWarnings("unchecked")
public class PrecisionAndRecallOpImage extends StatisticsOpImage {

//---------------------------
// Instance variables
//---------------------------

   /** The truth image. */
   private RenderedImage truth;
   /** The candidate color. */
   private int[] candidate_color;
   /** The truth color. */
   private int[] truth_color;

   /** Indicates that the two statistics have been accumulated. */
   private boolean accumulated = false;
   /** The precision statistics. */
   private double precision = 0.0;
   /** The recall statistics. */
   private double recall = 0.0;


//---------------------------
// Constructors
//---------------------------

   /**
    * Constructor.
    * @param   candidate   the candidate image.
    * @param   c_color   the color of the class in the candidate image.
    * @param   truth   the truth image.
    * @param   t_color   the color of the class in the truth image.
    * @param   roi   the region of interest.
    * @param   h_start   the horizontal starting position.
    * @param   v_start   the vertical starting position.
    * @param   h_sampling   the horizontal sampling rate.
    * @param   v_sampling   the vertical sampling rate.
    */
   public PrecisionAndRecallOpImage(RenderedImage candidate, int[] c_color,
                                    RenderedImage truth, int[] t_color,
                                    ROI roi,
                                    int h_start, int v_start,
                                    int h_sampling, int v_sampling) {
      super(candidate, roi, h_start, v_start, h_sampling, v_sampling);
      this.setup(truth, c_color, t_color);
      }


//---------------------------
// Accessors and mutators
//---------------------------

   /**
    * Gets the region of interest of this image.
    * @return   the region of interest.
    */
   protected ROI getRegionOfInterest() {
      return this.roi;
      }

   /**
    * Gets the horizontal starting position of the pixels in this image.
    * @return   the starting position.
    */
   protected int getHorizontalStartingPosition() {
      return this.xStart;
      }

   /**
    * Gets the horizontal sampling rate of the pixels in this image.
    * @return   the sampling rate.
    */
   protected int getHorizontalSamplingRate() {
      return this.xPeriod;
      }

   /**
    * Gets the vertical starting position of the pixels in this image.
    * @return   the starting position.
    */
   protected int getVerticalStartingPosition() {
      return this.yStart;
      }

   /**
    * Gets the vertical sampling rate of the pixels in this image.
    * @return   the sampling rate.
    */
   protected int getVerticalSamplingRate() {
      return this.yPeriod;
      }

   /**
    * Changes the truth image.
    * @param   image   the new truth image.
    */
   protected void setTruthImage(RenderedImage image) {
      this.truth = image;
      }

   /**
    * Gets the truth image.
    * @return   the new truth image.
    */
   protected RenderedImage getTruthImage() {
      return this.truth;
      }

   /**
    * Changes the color of the class in the truth image.
    * @param   color   the new color.
    */
   protected void setTruthColor(int[] color) {
      this.truth_color = color;
      }

   /**
    * Gets the color of the class in the truth image.
    * @return   the color.
    */
   protected int[] getTruthColor() {
      return this.truth_color;
      }

   /**
    * Changes the color of the class in the candidate image.
    * @param   color   the new color.
    */
   protected void setCandidateColor(int[] color) {
      this.candidate_color = color;
      }

   /**
    * Gets the color of the class in the candidate image.
    * @return   the color.
    */
   protected int[] getCandidateColor() {
      return this.candidate_color;
      }

   /**
    * Marks the statistics as accumulated.
    */
   protected void markAccumulated() {
      this.accumulated = true;
      }

   /**
    * Determines if the statistics have been accumulated.
    * @return   {@code true} if the statistics have been accumulated,
    *           {@code false} otherwise.
    */
   protected boolean areAccumulated() {
      return this.accumulated;
      }

   /**
    * Changes the precision.
    * @param   precision   the new precision.
    */
   protected void setPrecision(double precision) {
      this.precision = precision;
      }

   /**
    * Gets the precision.
    * @return   the precision.
    */
   protected double getPrecision() {
      return this.precision;
      }

   /**
    * Changes the recall.
    * @param   recall   the new recall.
    */
   protected void setRecall(double recall) {
      this.recall = recall;
      }

   /**
    * Gets the recall.
    * @return   the recall.
    */
   protected double getRecall() {
      return this.recall;
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Sets up this image.
    * @param   truth   the truth image.
    * @param   c_color   the color of the class in the candidate image.
    * @param   t_color   the color of the class in the truth image.
    */
   protected void setup(RenderedImage truth, int[] c_color, int[] t_color) {
      this.setCandidateColor(c_color);
      this.setTruthImage(truth);
      this.setTruthColor(t_color);
      }

   /**
    * Gets a list of {@link Rectangle}s that forms the intersection of the
    * raster and the region of interest. Taken from {@link ExtremaOpImage}.
    * @param   raster   the raster.
    * @param   roi   the region of interest.
    * @return   a list of rectangle.
    */
   @SuppressWarnings("hiding")
   protected List<Rectangle> getROIAsRectangles(Raster raster, ROI roi) {
      Rectangle bounds = raster.getBounds();

      int x = bounds.x;
      int y = bounds.y;
      int w = bounds.width;
      int h = bounds.height;

      List<Rectangle> list = roi.getAsRectangleList(x, y, w, h);
      if (null == list) {
         list = new LinkedList<Rectangle>();
         }
      return list;
      }

   /**
    * Calculate the starting postion according to the boundaries of the image
    * and the region of interest. Taken from {@link ExtremaOpImage}.
    * @param   pos   the starting position of the region of interest.
    * @param   start   the global starting position.
    * @param   period   the sampling rate.
    * @return   the real starting position.
    */
   protected int startPosition(int pos, int start, int period) {
      int t = (pos - start) % period;
      return t == 0 ? pos : pos + (period - t);
      }

   /**
    * Gets the horizontal starting position.
    * @param   roi   the region of interest.
    * @return   the starting position.
    */
   @SuppressWarnings("hiding")
   protected int getHorizontalStartPosition(Rectangle roi) {
      int begin_roi = roi.x;
      int begin_global = this.getHorizontalStartingPosition();
      int period = this.getHorizontalSamplingRate();
      int start = this.startPosition(begin_roi, begin_global, period);
      return start;
      }

   /**
    * Gets the vertical starting position.
    * @param   roi   the region of interest.
    * @return   the starting position.
    */
   @SuppressWarnings("hiding")
   protected int getVerticalStartPosition(Rectangle roi) {
      int begin_roi = roi.y;
      int begin_global = this.getVerticalStartingPosition();
      int period = this.getVerticalSamplingRate();
      int start = this.startPosition(begin_roi, begin_global, period);
      return start;
      }

   /**
    * Gets the horizontal stopping position. This pixel will NOT be sampled.
    * @param   roi   the region of interest.
    * @return   the stopping position.
    */
   @SuppressWarnings("hiding")
   protected int getHorizontalStopPosition(Rectangle roi) {
      int begin_roi = roi.x;
      int width_roi = roi.width;
      int stop = begin_roi + width_roi;
      return stop;
      }

   /**
    * Gets the vertical stopping position. This pixel will NOT be sampled.
    * @param   roi   the region of interest.
    * @return   the stopping position.
    */
   @SuppressWarnings("hiding")
   protected int getVerticalStopPosition(Rectangle roi) {
      int begin_roi = roi.y;
      int width_roi = roi.height;
      int stop = begin_roi + width_roi;
      return stop;
      }


//---------------------------
// Overridden methods from javax.media.jai.StatisticsOpImage
//---------------------------

   /**
    * Gets a property of this image.
    * @param   name   the name of the property.
    * @return   the property.
    */
   @Override
   @SuppressWarnings("hiding")
   public Object getProperty(String name) {
      Object property;

   // if the statistics have already been accumulated, re-use them
      boolean are_accumulated = this.areAccumulated();
      if (are_accumulated && name.equalsIgnoreCase(PrecisionAndRecallDescriptor.PRECISION_STATISTIC_NAME)) {
         double precision = this.getPrecision();
         property = new double[] { precision, };
         }

      if (are_accumulated && name.equalsIgnoreCase(PrecisionAndRecallDescriptor.RECALL_STATISTIC_NAME)) {
         double recall = this.getRecall();
         property = new double[] { recall, };
         }

      property = super.getProperty(name);

      return property;
      }

   /**
    * Accumulates the statistics.
    * @param   name   the name of the statistics.
    * @param   candidate   the raster of the candidate image.
    * @param   object   the return object for the statistics.
    */
   @Override
   @SuppressWarnings("hiding")
   protected void accumulateStatistics(String name, Raster candidate, Object object) {
      RenderedImage truth_image = this.getTruthImage();
      Raster truth = truth_image.getData();

      int[] cclass = this.getCandidateColor();
      int[] tclass = this.getTruthColor();

      ROI roi = this.getRegionOfInterest();
      List<Rectangle> rectangles = this.getROIAsRectangles(candidate, roi);

      int total_candidate_color = 0;
      int total_truth_color = 0;
      int total_both_color = 0;

      for (Rectangle rectangle : rectangles) {
         int start_x = this.getHorizontalStartPosition(rectangle);
         int start_y = this.getVerticalStartPosition(rectangle);

         int stop_x = this.getHorizontalStopPosition(rectangle);
         int stop_y = this.getVerticalStopPosition(rectangle);

         int step_x = this.getHorizontalSamplingRate();
         int step_y = this.getVerticalSamplingRate();

         int[] ccolor = null;
         int[] tcolor = null;

         for (int y = start_y; y < stop_y; y += step_y) {
            for (int x = start_x; x < stop_x; x += step_x) {
               ccolor = candidate.getPixel(x, y, ccolor);
               tcolor = truth.getPixel(x, y, tcolor);

               boolean is_candidate_color = Arrays.equals(ccolor, cclass);
               boolean is_truth_color = Arrays.equals(tcolor, tclass);

               if (is_candidate_color) {
                  total_candidate_color++;
                  }

               if (is_truth_color) {
                  total_truth_color++;
                  }

               if (is_candidate_color && is_truth_color) {
                  total_both_color++;
                  }
               }
            }
         }

      double tcc = total_candidate_color;
      double ttc = total_truth_color;
      double tbc = total_both_color;

      double precision = tbc / tcc;
      double recall = tbc / ttc;

      this.setPrecision(precision);
      this.setRecall(recall);

      if (name.equalsIgnoreCase(PrecisionAndRecallDescriptor.PRECISION_STATISTIC_NAME)) {
         double[] stats = (double[]) object;
         stats[0] = precision;
         }

      if (name.equalsIgnoreCase(PrecisionAndRecallDescriptor.RECALL_STATISTIC_NAME)) {
         double[] stats = (double[]) object;
         stats[0] = recall;
         }

//      System.out.println("tcc = " + total_candidate_color);
//      System.out.println("ttc = " + total_truth_color);
//      System.out.println("tbc = " + total_both_color);
//      System.out.println("precision = " + precision);
//      System.out.println("recall    = " + recall);
//      System.out.println("f-measure = " + (2*precision*recall)/(precision+recall));
      }

   /**
    * Creates the statistics.
    * @param   name   the name of the statistics.
    * @return   a {@code double[1]}.
    */
   @Override
   protected Object createStatistics(String name) {
      Object stats = Image.UndefinedProperty;

      if (name.equalsIgnoreCase(PrecisionAndRecallDescriptor.PRECISION_STATISTIC_NAME)) {
         stats = new double[1];
         }
      if (name.equalsIgnoreCase(PrecisionAndRecallDescriptor.RECALL_STATISTIC_NAME)) {
         stats = new double[1];
         }

      return stats;
      }

   /**
    * Get the name of the statistics this {@code OpImage} is creating, only
    * {@code PrecisionAndRecallDescriptor.PRECISION_STATISTIC_NAME}.
    * @return   the name of the statistic.
    * @see   PrecisionAndRecallDescriptor#PRECISION_STATISTIC_NAME
    */
   @Override
   protected String[] getStatisticsNames() {
      String[] names = new String[] {
         PrecisionAndRecallDescriptor.PRECISION_STATISTIC_NAME,
         PrecisionAndRecallDescriptor.RECALL_STATISTIC_NAME,
         };
      return names;
      }

   }
